<?php
/**
 * @version $Id: trackback.php 181 2008-03-11 21:46:28Z mikhail.miguel $
 */
/**
 * Services_Trackback.
 *
 * This is the main file of the Services_Trackback package. This file has to be
 * included for usage of Services_Trackback.
 *
 * PHP versions 4 and 5
 *
 * LICENSE: This source file is subject to version 3.0 of the PHP license
 * that is available through the world-wide-web at the following URI:
 * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
 * the PHP License and are unable to obtain it through the web, please
 * send a note to license@php.net so we can mail you a copy immediately.
 *
 * @category   Webservices
 * @package    Trackback
 * @author     Tobias Schlitt <toby@php.net>
 * @copyright  2005-2006 The PHP Group
 * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
 * @link       http://pear.php.net/package/Services_Trackback
 * @since      File available since Release 0.1.0
 */
require_once XOOPS_ROOT_PATH.'/class/snoopy.php';
require_once dirname(dirname(dirname(__FILE__))).'/lib/trackback/spamcheck.php';

class TrackbackService {
    var $_data = array(
        'id'            => '',
        'title'         => '',
        'excerpt'       => '',
        'blog_name'     => '',
        'url'           => '',
        'trackback_url' => '',
        'host'          => '',
        'charset'       => 'UTF-8',
    );
    var $_options = array(
        'proxy_host' => '',
        'proxy_port' => '',
        'proxy_user' => '',
        'proxy_pass' => '',
        'max_redirect' => 0,
        'offsiteok' => true
    );
    var $_spamChecks = array();
    // updateping
    var $_updateping_body;
    var $_updateping_headers;

    /**
     *
     */
    function TrackbackService () {
        $this->_data = array();
    }
    
    /**
     * autodiscover
     * Checks a given URL for trackback autodiscovery code.
     * @return bool True on success, otherwise TrackbackServiceError.
     */
    function autodiscover() {
        $necessaryData = array('url');
        $res = $this->_checkData($necessaryData);
        if($this->isError($res)) {
            return $res;
        }
        $url = $this->_data['url'];

        // Receive file contents.
        $content = $this->getContent($url);

        if ($this->isError($content)) {
            return $content;
        }
        
        // Get trackback identifier
        if (!preg_match('@dc:identifier\s*=\s*["\'](http:[^"\']+)"@i', $content, $matches)) {
            return $this->setError('No trackback RDF found in "'.$url.'".');
        }
        $identifier = trim($matches[1]);
        
        // Get trackback URI
        if (!preg_match('@trackback:ping\s*=\s*["\'](http:[^"\']+)"@i', $content, $matches)) {
            return $this->setError('No trackback URI found in "'.$url.'".');
        }
        $trackbackUrl = trim($matches[1]);

        // Check if the URL to trackback matches the identifier from the autodiscovery code
        $res = $this->_checkURLs($url, $identifier);
        if ($this->isError($res)) {
            return $res;
        }
        
        return $trackbackUrl;
    }

    /**
     * @param string $url url to send trackback to
     * @return Void mixed boolean True on success, otherwise TrackbackServiceError.
    */
    function send($url) {
        $necessaryData = array('url', 'blog_name', 'title', 'excerpt');
        $res = $this->_checkData($necessaryData);
        if($this->isError($res)) {
            return $res;
        }

        $snoopy = new Snoopy();

        if (!$snoopy->submit($url, $this->_data)) {
            return $this->setError('Host returned Error:'.$snoopy->error);
        }
        return $this->_interpretTrackbackResponse($snoopy->results);
    }

    /**
     * @param array $data data array to receive
     * @return bool
     */
    function receive($data=null)
    {
        if (!isset($data)) {
            $data = $_POST;
            $data['host'] = $_SERVER['REMOTE_ADDR'];
        }
/*
        $res = $this->_checkData(array('id'), $this->_data);
        if ($this->isError($res)) {
            return $res;
       }
*/
        $necessaryPostData = array('title', 'excerpt', 'url', 'blog_name', 'host');
        $res = $this->_checkData($necessaryPostData, $data);
        if ($this->isError($res)) {
            return $this->setError('POST data incomplete: '.$res->getMessage());
        }
        $this->_data['charset'] = $this->_detectEncoding();
        $converted = $this->_convertEncoding($necessaryPostData, $data, $this->_data['charset']);
        $this->_data = array_merge($this->_data, $converted);
        return true;
    }

    /**
     * Get TrackBack ID from PATH_INFO
     * @return mixed TrackBack ID or TrackbackServiceError on failure
     */
    function getId() {
        if (array_key_exists('PATH_INFO', $_SERVER)) {
            $pathinfo = $_SERVER['PATH_INFO'];
        } else {
            $pathinfo = '';
        }

        if (substr($pathinfo, 0, 1) == '/') {
            $id = substr($pathinfo, 1);
        } else {
            return $this->setError('Invalid PATH_INFO');
        }

        $pos = strpos($id, '/');
        if ($pos != false) {
            $id = substr($id, 0, $pos);
        }
        return $id;
    }

    function getPingList($rss)
    {
        $necessaryData = array('title', 'url', 'excerpt', 'language', 'encoding' );
        $res = $this->_checkData($necessaryData);
        if ($this->isError($res)) {
            return $res;
        }
        $data = $this->_getEncodedData($necessaryData);
        // each rss
        $items = '';
        if(is_array($rss)) {
            $item = <<<EOF
<item>
<title>%s</title>
<link>%s</link>
<description>%s</description>
</item>      
EOF;
            foreach($rss as $r) {
                $encoded = $this->_getEncodedData(array_keys($r), $r);
                $items .= sprintf($item, $encoded['title'], $encoded['url'], $encoded['excerpt']);
            }
        }
        
        $res = <<<EOD
<?xml version="1.0" encoding="%s"?>
<response>
<error>0</error>
<rss version="0.91">
<channel>
<title>%s</title>
<link>%s</link>
<description>%s</description>
<language>%s</language>
%s
</channel>
</rss>
</response>
EOD;
        return sprintf($res, $data['encoding'], $data['title'], $data['url'], $data['excerpt'],
                              $data['language'], $items);

    }

    /**
     * weblogUpdateping
     */
    function sendWeblogUpdateping($parsed_url, $timeout = 0) {
        $rpc_server = $parsed_url['scheme'].$parsed_url['host'].$parsed_url['path'];
        $fp = fsockopen($parsed_url['host'], 80, $errno, $errstr, $timeout);

        if (!$fp) {
            return $this->setError('Connection to RPC server ' 
                            . htmlspecialchars($rpc_server, ENT_QUOTES) . ':' . 80
                            . ' failed. ' . $errstr);
        }

        if ($timeout) {
            stream_set_timeout($fp, $timeout);
        }

        $op  = $this->_updateping_headers . "\r\n\r\n" . $this->_updateping_body;
        if(!fputs($fp, $op, strlen($op))) {
        	return $this->setError('Transmission to RPC server ' 
                            . htmlspecialchars($rpc_server, ENT_QUOTES) . ':' . 80
                            . ' failed. ');
        }
//        $response = fread($fp , 4095);
        $meta = stream_get_meta_data($fp);
        if ($meta['timed_out']) {
            fclose($fp);
            return $this->setError('RPC server:'.htmlspecialchars($rpc_server, ENT_QUOTES).' did not send response before timeout.');
        }
        //error_log($response . "\n" , 3 , 'd:/tmp/debug.log');
        fclose($fp);
        return true;
    }

    function _getUpdatepingBody() {
        $necessaryData = array('url', 'blog_name');
        $res = $this->_checkData($necessaryData);
        if($this->isError($res)) {
            return $res;
        }       
        $data = $this->_getEncodedData($necessaryData);
        // each rss
        $body = <<<EOD
<?xml version="1.0" encoding="UTF-8" ?>
<methodCall>
<methodName>weblogUpdates.ping</methodName>
<params>
<param>
<value>%s</value>
</param>
<param>
<value>%s</value>
</param>
</params>
</methodCall>
EOD;

         $this->_updateping_body = str_replace("\n", "\r\n", sprintf($body, $data['blog_name'], $data['url']));
         return true;
    }

    function _getUpdatepingHeader($parsed_url) {
        $this->_updateping_headers = 'POST ';
        $this->_updateping_headers .= $parsed_url['path']. " HTTP/1.0\r\n";
        $this->_updateping_headers .= "User-Agent: XOOPSCUBE D3BLOG\r\n";
        $this->_updateping_headers .= 'Host: ' . $parsed_url['host'] . "\r\n";
        $this->_updateping_headers .= "Content-Type: text/xml\r\n";
        $this->_updateping_headers .= 'Content-Length: ' . strlen($this->_updateping_body);
        return true;        
    }

    function _convertEncoding($keys, $data=null, $charset)
    {
        if (!isset($data)) {
            $data =& $this->_data;
        }
        foreach ($keys as $key) {
            $res[$key] = d3blog_encoding($data[$key], $charset);
        }
        return $res;
    }

    /**
     * Receives a number of data from the internal data store, encoded for XML usage.
     * @param array $keys Data keys to receive
     * @return void
     */
    function _getEncodedData($keys, $data=null)
    {
        if (!isset($data)) {
            $data =& $this->_data;
        }
        foreach ($keys as $key) {
            $res[$key] = htmlspecialchars(xoops_convert_encoding($data[$key]), ENT_QUOTES);
        }
        return $res;
    }

    /**
     * Receives a number of data from the internal data store.
     * @param array $keys Data keys to receive
     * @param array $data Optionally the data to check (default is the object data).
     * @return void
     */
    function _getDecodedData($keys, $data=null)
    {
        if (!isset($data)) {
            $data =& $this->_data;
        }
        foreach ($keys as $key) {
            $res[$key] = $data[$key];
        }
        return $res;
    }

    function _checkData($keys, $data=null)
    {
        if (!isset($data)) {
            $data =& $this->_data;
        }
        foreach ($keys as $key) {
            if (empty($data[$key])) {
                return $this->setError('Invalid data. Key '.$key.' is missing.');
            }
        }
        return true;
    }

    function getContent($url) {
    	// get the remote site contents
        $snoopy = new Snoopy;
        if ($snoopy->fetch($url)) {
            $contents = $snoopy->results;
        } else {
            return $this->setError('Could not get remote site contents.');
        }
        return $contents;
    }

    /**
     * Interprete the returned XML code, when sending a trackback. 
     * @return void Mixed true on success, otherwise TrackbackServiceError.
     */
    function _interpretTrackbackResponse($response)
    {
        if (!preg_match('@<error>([0-9]+)</error>@', $response, $matches)) {
            return $this->setError('Invalid trackback response, error code not found.');
        }
        $errorCode = $matches[1];
        
        // Error code 0 means no error.
        if ($errorCode == 0) {
            return true;
        }
        
        if (!preg_match('@<message>([^<]+)</message>@', $response, $matches)) {
            return $this->setError('Error code '.$errorCode.', no message received.');
        }

        return $this->setError('Error code '.$errorCode.', message "'.$matches[1].'" received.');
    }

    /**
     * @param string $url1 The first URL.
     * @param string $url2 The second URL.
     * @return mixed True on success, otherwise TrackbackServiceError object.
     */
    function _checkURLs($url1, $url2=null)
    {
        if(!d3blog_validateUrl($url1)) {
        	return $this->setError('URL is invalid. "'.$url1);
        }
        if (isset($url2) && $url1 !== $url2) {
            return $this->setError('URLs mismatch. "'.$url1.'" !== "'.$url2.'".');
        }
        return true;
    }

    function _detectEncoding()
    {
        return d3blog_detectEncoding($_POST['title'].$_POST['excerpt'].$_POST['blog_name']);
    }

    function get($key)
    {
        return (isset($this->_data[$key])) ? $this->_data[$key] : $this->setError('Key '.$key.' not found.');
    }

    function &createSpamCheck($spamCheckType, $options=array())
    {
        $filename = dirname(dirname(__FILE__)).'/lib/spamcheck.php';

        $createfunc = array('SpamCheck', 'create');
        
        // SpamCheck class already included?
        if (!class_exists($createfunc[0])) {
            if (!file_exists($filename)) {
                return $this->setError('SpamCheck subclass not found. Broken installation!');
            } else {
                include_once $filename;
            }
        }

        // SpamCheck class successfully included?
        if (!class_exists($createfunc[0])) {
            return $this->setError('SpamCheck subclass not found. Broken installation!');
        }
        
        $spamCheck = call_user_func($createfunc, $spamCheckType, $options);
        if($this->isError($spamCheck)) {
        	return $spamCheck;
        }
        $res =& $this->addSpamCheck($spamCheck);
        return $res;
    }
    
    function &addSpamCheck(&$spamCheck)
    {
        if (!is_object($spamCheck) || !is_subclass_of($spamCheck, 'SpamCheck')) {
            return $this->setError('Invalid spam check module.');
        }
        $this->_spamChecks[] =& $spamCheck;
        return $spamCheck;
    }
        
    function checkSpam()
    {
        foreach (array_keys($this->_spamChecks) as $i) {
        	$spamCheck =& $this->_spamChecks[$i];
            if($this->isError($result = $spamCheck->check($this))) {
//error_log($result->getMessage()."\n", 3, '/tmp/debug.log');
                return $result;
            }
        }
        return true;
    }

    function serverErrorWarning($error) {
    	$this->_sendWarningMail($error);
        exit(d3blog_responseError('Sorry, server error occurred', 1));
    }

    function _sendWarningMail($error)
    {
        global $xoopsModule;
        $sendaddress = @$GLOBALS['xoopsConfig']['adminmail'];
        $body = "[result]" . 'Server error occurred: '. $error->getMessage() . "\n";
        $xoopsMailer = & getMailer();
        $xoopsMailer->useMail();
        $xoopsMailer->setToEmails( $sendaddress );
        $xoopsMailer->setFromEmail( $sendaddress );
        $xoopsMailer->setFromName( 'D3blog Admin' );
        $xoopsMailer->setSubject( 'Server error occurred on:'.$xoopsModule->getVar('name') );
        $xoopsMailer->setBody( $body );
        $xoopsMailer->send();
        return true;
    }
    
    /**
     * add an error
     * @param string $message error to add
     * @return object
     */
    function setError($message) {
    	return new TrackbackServiceError($message);
    }

    function isError($result) {
    	if(isset($result) && is_a($result, 'TrackbackServiceError'))
    		return true;
        else
            return false;
    }

}

class TrackbackServiceError {
    var $_message;
    function TrackbackServiceError($message) {
    	$this->_message = trim($message);
    }
    function getMessage() {
//    	if(isset($this) && is_a($this, 'TrackbackServiceError')) {
        	return htmlspecialchars($this->_message, ENT_QUOTES);
/*        } else {
        	return false;
        }*/
    }	
}
?>